/*
 * Copyright (c) 2009 - 2010 Plausible Labs Cooperative, Inc.
 * Copyright (c) 2009 Remy Demarest
 * All rights reserved.
 *
 * Permission is hereby granted, free of charge,
 * to any person obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without restriction,
 * including without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to permit
 * persons to whom the Software is furnished to do so, subject to the following
 * conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

#import <TargetConditionals.h>

#if TARGET_OS_IPHONE
#import "GTMSenTestCase.h"
#import <UIKit/UIKit.h>
#else
#import <SenTestingKit/SenTestingKit.h>
#endif

#import "Block.h"
#import "Block_private.h"
#import "NSBlock.h"

#import <objc/objc-auto.h>

#import "BlockCTests.h"
#import "BlockCPPTests.h"

typedef void (^PLBasicBlock)();

/* An object used to track _PLBlock_object_assign/_PLBlock_object_dispose reference or GC handling */
@interface PLReferenceCountObject : NSObject {
    void (^_onRelease)();
    void (^_onDealloc)();
    void (^_onFinalize)();
}
@end

@implementation PLReferenceCountObject
- (id) initWithReleaseBlock: (void (^)()) onRelease deallocBlock: (void (^)()) onDealloc finalizeBlock: (void (^)()) onFinalize {
    if ((self = [super init]) == nil)
        return nil;

    _onRelease = [onRelease copy];
    _onDealloc = [onDealloc copy];
    _onFinalize = [onFinalize copy];

    return self;
}

- (int) doSomething {
    return 42;
}

- (void) release {
    _onRelease();
    [super release];
}

- (void) dealloc {
    _onDealloc();
    [_onRelease release];
    [_onDealloc release];
    [_onFinalize release];
    [super dealloc];
}

- (void) finalize {
    _onFinalize();
    [super finalize];
}

@end

@interface BlockTests : SenTestCase @end

@implementation BlockTests

/* Block C support */
- (void) testBlockCSupport {
    CFStringRef error;

    STAssertTrue(BlockCTests(&error), @"C block tests failed: %@", error);
}


/* Block C++ support */
- (void) testBlockCPPSupport {
    CFStringRef error;
    
    STAssertTrue(BlockCPPTests(&error), @"C++ block tests failed: %@", error);
}

/* Test delegation to the Apple runtime */
- (void) testAppleRuntimeDelegation {
    /*
     * Verify that the runtime is correctly enabled
     */
    #if TARGET_OS_MAC && !TARGET_OS_IPHONE
    if (_Block_copy != NULL)
        STAssertTrue(PLBlock_AppleRuntimeEnabled, @"Runtime not enabled on 10.6+");
    #endif
        
    /* On iPhone OS, block symbols were available as early as iPhone OS 3.2, but are broken
     * and considered private. We have to check the OS version to verify that we're running on
     * 4.0 or later before enabling fallback to Apple's runtime. */
    #if TARGET_OS_IPHONE    
    NSString *currentIOS = [[UIDevice currentDevice] systemVersion];
    if (_Block_copy != NULL && ![currentIOS isEqual: @"3.2"])
        STAssertTrue(PLBlock_AppleRuntimeEnabled, @"Runtime not enabled on 4.0+ device");
    #endif


    /* Test the block class types */
    int testVal = 5;
    void (^StackBlock)() = ^{
        NSLog(@"%d", testVal);
    };
    
    void (^GlobalBlock)() = ^{};

    /* Trigger isa swapping via ObjC copy */
    id stackCopy = [[StackBlock copy] autorelease];
    id globalCopy = [[GlobalBlock copy] autorelease];

    STAssertTrue([StackBlock isKindOfClass: [PLBlock class]] == !PLBlock_AppleRuntimeEnabled, @"Unexpected block class, isa not swapped");
    STAssertTrue([GlobalBlock isKindOfClass: [PLBlock class]] == !PLBlock_AppleRuntimeEnabled, @"Unexpected block class, isa not swapped");
    
    STAssertTrue([stackCopy isKindOfClass: [PLBlock class]] == !PLBlock_AppleRuntimeEnabled, @"Unexpected block class, isa not swapped");
    STAssertTrue([globalCopy isKindOfClass: [PLBlock class]] == !PLBlock_AppleRuntimeEnabled, @"Unexpected block class, isa not swapped");

    /* Trigger isa swapping via C copy */
    stackCopy = PLBlock_copy(StackBlock);
    globalCopy = PLBlock_copy(GlobalBlock);

    STAssertTrue([stackCopy isKindOfClass: [PLBlock class]] == !PLBlock_AppleRuntimeEnabled, @"Unexpected block class, isa not swapped");
    STAssertTrue([globalCopy isKindOfClass: [PLBlock class]] == !PLBlock_AppleRuntimeEnabled, @"Unexpected block class, isa not swapped");

    PLBlock_release(stackCopy);
    PLBlock_release(globalCopy);
}

/* Block Objective-C reference counting */
- (void) testBlockObjCReferenceCounting {
    if (objc_collectingEnabled())
        return;

    __block BOOL releaseCalled = NO;
    __block BOOL deallocCalled = NO;
    __block BOOL finalizeCalled = NO;

    /* Create the release tracking block */
    PLReferenceCountObject *obj = [[PLReferenceCountObject alloc] initWithReleaseBlock: ^{
        releaseCalled = YES;
    } deallocBlock: ^{
        deallocCalled = YES;
    } finalizeBlock: ^{
        finalizeCalled = YES;
    }];
    
    /* Capture the refcount tracker in a copied block */
    STAssertEquals([obj doSomething], 42, @"Method did not return 42");
    void (^b)() = PLBlock_copy(^{
        STAssertEquals([obj doSomething], 42, @"Method did not return 42");
    });

    /* Execute the block */
    b();

    /* Release the object, and the block that has captured it*/
    STAssertFalse(releaseCalled, @"The block-retained object has already been released");
    PLBlock_release(b);
    STAssertTrue(releaseCalled, @"The block-retained object has not been released");
    STAssertFalse(deallocCalled, @"The block-retained object has already been dealloced");
    [obj release];
    STAssertTrue(deallocCalled, @"The block-retained object was not released");
    STAssertFalse(finalizeCalled, @"Finalize was called on a non-GC block");
}

/* Block Objective-C casting */
- (void) testBlockObjC {
    NSString *value = @"String";
    
    NSString *(^b)() = ^{
        return value;
    };
    
    NSString *(^copy)() = [b copy];
    STAssertEquals(@"String", copy(), @"Block did not return expected value.");
    [copy autorelease];
}

/* A block copy */
- (void) testBlockCopy {
    NSString *value = @"String";
    
    NSString *(^b)() = ^{
        return value;
    };

    NSString *(^copy)() = PLBlock_copy(b);
    STAssertEquals(@"String", copy(), @"Block did not return expected value.");
    PLBlock_release(copy);
}

/* A block capturing Objective-C types */
- (void) testObjectCapture {
    NSString *value = @"String";
    
    NSString *(^b)() = ^{
        return value;
    };
    
    STAssertEquals(@"String", b(), @"Block did not return expected value.");
}

/* A block capturing primitive variable types */
- (void) testPrimitiveCapture {
    int value = 5;

    int (^b)() = ^{
        return value;
    };

    STAssertEquals(5, b(), @"Block did not return expected value.");
}

/* Simple block, capturing no variables */
- (void) testNoCapture {
    int (^b)(int) = ^(int v){ 
        return v; 
    };
    STAssertEquals(3, b(3), @"Block did not return expected value.");
}

#ifdef __OBJC_GC__

/*
 * Create a block that holds a reference to an Objective-C object for use by the testBlockGarbageReferenceCollection test.
 * This is done in a seperate method to avoid any possibility of dangling stack references, etc. */
- (void (^)()) createGCReferenceCountObject: (BOOL *) finalizeCalled {
    PLReferenceCountObject *obj = [[PLReferenceCountObject alloc] initWithReleaseBlock: ^{ 
        STFail(@"Release called on GC block"); 
    } deallocBlock: ^{
        STFail(@"Dealloc called on GC block");
    } finalizeBlock: ^{
        *finalizeCalled = YES;
    }];

    /* Capture the GC tracker in a copied block */
    return [^{
        STAssertEquals([obj doSomething], 42, @"Method did not return 42");
    } copy];
}


/* Block GC -- Verify that objects referenced by the block are both held and deallocated as necessary */
- (void) testBlockGarbageReferenceCollection {
    if (!objc_collectingEnabled())
        return;

    BOOL finalizeCalled = NO;

    /* Create a copied block that captures a standard Objective-C instance. */
    [self createGCReferenceCountObject: &finalizeCalled];
    objc_clear_stack(0);

    /** Once the block reference is dropped, ensure that the block captured object instance is deallocated */
    while (!finalizeCalled)
        [[NSGarbageCollector defaultCollector] collectExhaustively];

    STAssertTrue(finalizeCalled, @"Object was not deallocated");
}

/*
 * Creates a copied block that captures a weak Objective-C instance.
 */
- (PLBasicBlock) createGCWeakReferenceBlock: (BOOL *) lostWeakReference {
    __block __weak id obj = [NSObject new];

    return (PLBasicBlock) [^{
        if (obj == nil)
            *lostWeakReference = YES;

        return;
    } copy];
}

/* Test handling of __weak __block references */
- (void) testBlockWeakRefereces {
    if (!objc_collectingEnabled())
        return;

    /* Create the block and wait for the weak reference to be lost */
    BOOL lostWeak = NO;
    PLBasicBlock b = [self createGCWeakReferenceBlock: &lostWeak];
    
    /* Verify that the reference is valid prior to collection (WARN: this test is non-deterministic, collection may occur
     * prior to it!) */
    b();
    STAssertFalse(lostWeak, @"Weak reference was not copied");

    /* Now trigger exhaustive collection and verify that the weak reference is collected */
    while (!lostWeak) {
        b();
        [[NSGarbageCollector defaultCollector] collectExhaustively];
        sleep(1);
    }

    STAssertTrue(lostWeak, @"Weak reference was not lost");
}

#endif /* __OBJC_GC__ */


@end
